// AnalyticBMPApproximation.cpp : Defines the entry point for the application.
//

#include "stdafx.h"
#include "AnalyticBMPApproximation.h"
#include <commctrl.h>
#include <stdio.h>
#include <WindowsX.h>
#include <commdlg.h>
#include <string>

// Constants
#define MAX_LOADSTRING 100

// user messages
extern const int kWmStartUpdatingAnalytics = WM_USER;
extern const int kWmStartUpdatingCircle = WM_USER + 1;

static const DWORD kMaxRapidWheelInterval = 100; // milliseconds;

static const double kScales[] = { 1, 2, 5, 10, 20, 50 };

// Global Variables:
TCHAR gInitialTitle[MAX_LOADSTRING] = L"AnalyticBMPApproximation"; // The title bar text

// Forward declarations of functions included in this code module:
INT_PTR CALLBACK	About(HWND, UINT, WPARAM, LPARAM);

// ---- main entry to application ----
int APIENTRY _tWinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPTSTR    lpCmdLine,
                     int       nCmdShow)
{
	UNREFERENCED_PARAMETER(hPrevInstance);

	AnalyticBMPApproximation application;
	if (application.InitInstance(hInstance, nCmdShow))
	{
		application.OpenFile(lpCmdLine);
		return application.Run();
	}
	return 0;
}

//---------------------
AnalyticBMPApproximation::AnalyticBMPApproximation()
{
	mInstance = nullptr;
	mFrameWindow = mClientWindow = mStatusBarWindow = nullptr;
	mPreviousClientWidth = mPreviousClientHeight = 0;
	mScale = 1.0;
	mScaledSize.cx = mScaledSize.cy = 0;
	mInterpolateOriginalPixels = false;
	mWasMagnifierShown = false;
	mNumberOfRapidWheelEvents = 0;
	mTimeOfLastWheelEvent = ::GetTickCount() - kMaxRapidWheelInterval;
}

bool AnalyticBMPApproximation::InitInstance(HINSTANCE inInstance, int inCmdShow)
{
	bool result = false;

	if (!mImageKeeper.CreateGraphicsFactories())
	{
		return false;
	}

	mInstance = inInstance;
	WNDCLASS wc = { 0 };
	wc.hInstance = mInstance;
	wc.lpfnWndProc = AnalyticBMPApproximation::WndProcFrame;
	wc.lpszClassName = L"AnalyticBMPApproximationFrame";
	wc.lpszMenuName = MAKEINTRESOURCE(IDC_ANALYTICBMPAPPROXIMATION);
	wc.hIcon = LoadIcon(mInstance, MAKEINTRESOURCE(IDI_ANALYTICBMPAPPROXIMATION));
	wc.hCursor = LoadCursor(NULL, IDC_ARROW);
	if (ATOM frameWindowAtom = ::RegisterClass(&wc))
	{
		LoadString(mInstance, IDS_APP_TITLE, gInitialTitle, MAX_LOADSTRING);
		mFrameWindow = ::CreateWindowEx(0,
			(LPCTSTR)frameWindowAtom, gInitialTitle,
			WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0,
			nullptr, nullptr, mInstance, this);
	}
	if (mFrameWindow)
	{
		wc.hIcon = nullptr;
		wc.lpszMenuName = nullptr;
		wc.lpfnWndProc = AnalyticBMPApproximation::WndProcClient;
		wc.lpszClassName = L"AnalyticBMPApproximationClient";
		if (ATOM clientWindowAtom = ::RegisterClass(&wc))
		{
			mClientWindow = ::CreateWindow((LPCTSTR)clientWindowAtom, nullptr,
				WS_CHILD | WS_VISIBLE | WS_HSCROLL | WS_VSCROLL,
				0, 0, 0, 0, mFrameWindow, (HMENU)IDC_CLIENTWINDOW,
				mInstance, this);
		}
	}

	HWND magnifierWindow = nullptr;

	if (mClientWindow)
	{
		mStatusBarWindow = ::CreateWindow(STATUSCLASSNAME, NULL, WS_CHILD | WS_VISIBLE,
			0, 0, 0, 0, mFrameWindow, (HMENU)IDC_STATUSBAR, mInstance, nullptr);

		wc.lpfnWndProc = AnalyticBMPApproximation::WndProcMagnifier;
		wc.lpszClassName = L"AnalyticBMPApproximationMagnifier";
		if (ATOM magnifierWindowAtom = ::RegisterClass(&wc))
		{
			SIZE magSize = mImageKeeper.GetMagnifierSize();
			RECT magRect = { 0, 0, magSize.cx, magSize.cy };

			DWORD style = WS_SYSMENU | WS_CAPTION;
			DWORD exStyle = WS_EX_TOOLWINDOW | WS_EX_TOPMOST;
			::AdjustWindowRectEx(&magRect, style, FALSE, exStyle);
			magnifierWindow = ::CreateWindowEx(exStyle, (LPCTSTR)magnifierWindowAtom,
				L"Magnifier", style,
				0, 0, magRect.right - magRect.left, magRect.bottom - magRect.top,
				nullptr, nullptr, mInstance, this);
		}
	}

	if (magnifierWindow)
	{
		mImageKeeper.SetMagnifierWindow(magnifierWindow);

		HMENU menu = ::GetMenu(mFrameWindow);
		::CheckMenuItem(menu, IDM_SHOWSTATUSBAR, (MF_CHECKED | MF_BYCOMMAND));
		::CheckMenuRadioItem(menu, IDM_SIDEPIXELS_6, IDM_SIDEPIXELS_12,
			IDM_SIDEPIXELS_10, MF_BYCOMMAND);
		mImageKeeper.SetPixelCountPerMagnifierSide(10);
		::CheckMenuRadioItem(menu, IDM_DEGREE_6, IDM_DEGREE_21,
			IDM_DEGREE_15, MF_BYCOMMAND);
		mImageKeeper.SetDegree(15);

		::CheckMenuItem(menu, IDM_ANALYTIC, MF_UNCHECKED | MF_BYCOMMAND);
		mImageKeeper.ShowAnalytic(false);

		::CheckMenuRadioItem(menu, IDM_CIRCLEGREEN, IDM_CIRCLEDISCRETEFEW, IDM_CIRCLEGREEN, MF_BYCOMMAND);
		mImageKeeper.SetCircleAppearance(kCircleAppearanceGreenLine);

		::ShowWindow(mFrameWindow, inCmdShow);
		::UpdateWindow(mFrameWindow);
		::EnableMouseInPointer(true);
		result = true;
	}

	return result;
}

bool AnalyticBMPApproximation::OpenFile(LPCTSTR inPath)
{
	bool result = mImageKeeper.OpenFile(inPath);
	if (result)
	{
		// append the file name to the initial window title

		typedef std::basic_string<TCHAR> StringT;	
		StringT title(gInitialTitle);
		StringT name(inPath);
		size_t slashPosition = name.find_last_of(L"/\\");
		if (slashPosition != StringT::npos)
		{
			// erase up to slash, and the slash itself (+1)
			name.erase(0, slashPosition + 1);
		}
		title += L" - ";
		title += name;

		::SetWindowText(mFrameWindow, title.c_str());
		mIsMouseReflectedInMagnifier = true;
	}
	return result;
}

int AnalyticBMPApproximation::Run()
{
	HACCEL hAccelTable = LoadAccelerators(mInstance, 
		MAKEINTRESOURCE(IDC_ANALYTICBMPAPPROXIMATION));

	MSG msg;
	// Main message loop:
	while (GetMessage(&msg, NULL, 0, 0))
	{
		if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
		{
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
	}
	return (int)msg.wParam;
}

//---------------------

bool AnalyticBMPApproximation::LocateFile(LPWSTR ioPathBuffer, DWORD inBufferLength)
{
	ioPathBuffer[0] = L'\0';

	OPENFILENAME ofn;
	ZeroMemory(&ofn, sizeof(ofn));

	ofn.lStructSize = sizeof(ofn);
	ofn.hwndOwner = mFrameWindow;
	ofn.lpstrFilter = L"All Image Files\0"   L"*.bmp;*.dib;*.wdp;*.mdp;*.hdp;*.gif;*.png;*.jpg;*.jpeg;*.tif;*.ico\0"
		L"All Files\0"                    L"*.*\0"
		L"\0";
	ofn.lpstrFile = ioPathBuffer;
	ofn.nMaxFile = inBufferLength;
	ofn.lpstrTitle = L"Open Image";
	ofn.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST;

	// Display the Open dialog box. 
	return (GetOpenFileName(&ofn) == TRUE);
}

bool AnalyticBMPApproximation::UpdateClientSize(int inRawClientWidth,
	int inRawClientHeight)
{
	int clientHeight = inRawClientHeight;
	if (nullptr != mStatusBarWindow && ::IsWindowVisible(mStatusBarWindow))
	{
		RECT statusRect;
		if (::IsWindowVisible(mStatusBarWindow) &&
			::GetWindowRect(mStatusBarWindow, &statusRect))
		{
			clientHeight -= (statusRect.bottom - statusRect.top);
		}
	}

	// adjust the the client area
	BOOL isMoved = ::MoveWindow(mClientWindow, 0, 0, inRawClientWidth, clientHeight, false);

	// update nPage for scrollbars
	SetScrollBars(1.0);

	// mark update regions
	SCROLLBARINFO sbi;
	sbi.cbSize = sizeof(sbi);
	int scrollbarWidth = 0;
	if (::GetScrollBarInfo(mClientWindow, OBJID_VSCROLL, &sbi))
	{
		scrollbarWidth = sbi.rcScrollBar.right - sbi.rcScrollBar.left;
	}

	RECT r = {0, 0, inRawClientWidth, clientHeight};
	r.left = min(inRawClientWidth, mPreviousClientWidth) - scrollbarWidth;
	::InvalidateRect(mFrameWindow, &r, false);
	mPreviousClientWidth = inRawClientWidth;

	int scrollbarHeight = 0;
	if (::GetScrollBarInfo(mClientWindow, OBJID_HSCROLL, &sbi))
	{
		scrollbarHeight = sbi.rcScrollBar.bottom - sbi.rcScrollBar.top;
	}
	r.left = 0;
	r.top = min(clientHeight, mPreviousClientHeight) - scrollbarHeight;
	::InvalidateRect(mFrameWindow, &r, false);
	mPreviousClientHeight = clientHeight;
	return (0 != isMoved);
}

template<typename T>
void RestrictToBounds(T& ioValue, T inMin, T inMax)
{
	if (ioValue < inMin || inMax < inMin)
		ioValue = inMin;
	else if (ioValue > inMax)
		ioValue = inMax;
}

// approximately the center of the picture would be same while scaling
static void AdjustPos(SCROLLINFO& si, double inCoef, int inOffsetToMouse)
{
	int offset = (inOffsetToMouse == AnalyticBMPApproximation::kUseWindowCenter ?
		(si.nPage / 2) : inOffsetToMouse);
	si.nPos = int(si.nPos * inCoef + (inCoef - 1.0) * offset);
	RestrictToBounds(si.nPos, si.nMin, int(si.nMax + 1 - max(si.nPage, 1)));
}

void AnalyticBMPApproximation::SetScrollBars(double inCoef, int inOffsetX,
	int inOffsetY)
{
	RECT clientRect;
	if (!::GetClientRect(mClientWindow, &clientRect))
	{
		return;
	}

	SCROLLINFO si;
	si.cbSize = sizeof(si);
	si.fMask = (SIF_DISABLENOSCROLL | SIF_ALL);
	si.nMin = 0;

	si.nMax = mScaledSize.cx;
	si.nPage = clientRect.right;
	si.nPos = mScrollPosition.x;
	AdjustPos(si, inCoef, inOffsetX);
	::SetScrollInfo(mClientWindow, SB_HORZ, &si, TRUE);
	mScrollPosition.x = si.nPos;

	si.nMax = mScaledSize.cy;
	si.nPage = clientRect.bottom;
	si.nPos = mScrollPosition.y;
	AdjustPos(si, inCoef, inOffsetY);
	::SetScrollInfo(mClientWindow, SB_VERT, &si, TRUE);
	mScrollPosition.y = si.nPos;
}

int  AnalyticBMPApproximation::UpdateScrollbarPosition(int inNBar,
	int inCommand, int inThumbPosition)
{
	SCROLLINFO si;
	si.cbSize = sizeof(si);
	si.fMask = SIF_ALL;
	if (!::GetScrollInfo(mClientWindow, inNBar, &si))
	{
		return 0;
	}

	int newPos = si.nPos;
	int maxPos = si.nMax + 1 - max(si.nPage, 1);
	int lineDelta = max(10, (si.nPage / 10));
	switch (inCommand)
	{
	case SB_TOP: newPos = 0; break;
	case SB_BOTTOM: newPos = maxPos; break;
	case SB_PAGEUP: newPos = max(si.nMin, newPos - (int)si.nPage); break;
	case SB_PAGEDOWN: newPos = min(maxPos, newPos + (int)si.nPage); break;
	case SB_LINEUP: newPos = max(si.nMin, newPos - lineDelta); break;
	case SB_LINEDOWN: newPos = min(maxPos, newPos + lineDelta); break;
	case SB_THUMBTRACK: // fall through
	case SB_THUMBPOSITION: newPos = inThumbPosition; break;
	case SB_ENDSCROLL: break;
	}
	if (newPos != si.nPos)
	{
		::SetScrollPos(mClientWindow, inNBar, newPos, TRUE);
	}
	return newPos;
}

void AnalyticBMPApproximation::ShowMagnifier(bool inShow)
{
	HWND magnifierWnd = mImageKeeper.GetMagnifierWindow();
	if (inShow && !mWasMagnifierShown)
	{
		WINDOWINFO wi;
		wi.cbSize = sizeof(wi);
		RECT windowRect;
		if (::GetWindowInfo(mFrameWindow, &wi) && ::GetWindowRect(magnifierWnd, &windowRect))
		{
			int x = min(wi.rcClient.left + mScaledSize.cx, wi.rcClient.right);
			int y = wi.rcClient.top - 20;
			int magnifierWidth = windowRect.right - windowRect.left;

			if (HWND desktop = ::GetDesktopWindow())
			{
				RECT screenRect;
				if (::GetClientRect(desktop, &screenRect))
				{
					// not place the window beyond the right edge of screen
					int maxX = screenRect.right - magnifierWidth;
					if (x > maxX)
					{
						x = maxX;
					}
				}
			}

			::MoveWindow(magnifierWnd, x, y, magnifierWidth,
				windowRect.bottom - windowRect.top, false);
		}
	}
	::ShowWindow(magnifierWnd, inShow ? SW_SHOW : SW_HIDE);
}

void AnalyticBMPApproximation::ReflectMousePointer(LPARAM inLParam)
{
	int screenX = GET_X_LPARAM(inLParam);
	int screenY = GET_Y_LPARAM(inLParam);
	WINDOWINFO wi;
	wi.cbSize = sizeof(wi);
	::GetWindowInfo(mClientWindow, &wi);
	double x = (screenX - wi.rcClient.left + mScrollPosition.x) / mScale;
	double y = (screenY - wi.rcClient.top + mScrollPosition.y) / mScale;
	mImageKeeper.SetReflectingPointFromDevicePixels(x, y);

	ShowCoordinatesInStatusBar();
}

void AnalyticBMPApproximation::ShowCoordinatesInStatusBar()
{
	if (mStatusBarWindow)
	{
		double x, y;
		mImageKeeper.GetReflectingPoint(x, y);

		char buffer[100];
		::sprintf_s(buffer, "x = %.2lf, y = %.2lf", x, y);
		::SetWindowTextA(mStatusBarWindow, buffer);
	}
}

void AnalyticBMPApproximation::SetScaleAndRefresh(double inScale)
{
	mScale = inScale;
	::InvalidateRect(mClientWindow, nullptr, false);

	std::string text("scale ");
	text += std::to_string(lround(mScale * 100)) + "%";
	::SetWindowTextA(mStatusBarWindow, text.c_str());
}

void AnalyticBMPApproximation::RunContextMenu(int inSubmenuPosition,
	HWND inHWnd, LPARAM lParam)
{
	if (HMENU popupMenu = ::CreatePopupMenu())
	{
		if (HMENU mainMenu = ::GetMenu(mFrameWindow))
		{
			if (HMENU subMenu = ::GetSubMenu(mainMenu, inSubmenuPosition))
			{
				int count = ::GetMenuItemCount(subMenu);
				for (int i = 0; i < count; ++i)
				{
					UINT id = ::GetMenuItemID(subMenu, i);
					if (UINT(-1) == id || IDM_MAGNIFIER == id ||
						IDM_SHOWSTATUSBAR == id)
					{
						continue;
					}
					UINT state = ::GetMenuState(subMenu, i, MF_BYPOSITION);
					if ((0 == i || count - 1 == i) && 0 != (state & MF_SEPARATOR))
					{
						continue;
					}

					static TCHAR buf[100];
					int numberOfChars = ::GetMenuString(subMenu, i, buf, 100, MF_BYPOSITION);
					if (0 < numberOfChars || 0 != (state & MF_SEPARATOR))
					{
						::AppendMenu(popupMenu, 
							(state & (MF_CHECKED | MF_SEPARATOR)), id, buf);
					}
				}
			}
		}

		if (0 < ::GetMenuItemCount(popupMenu))
		{
			::TrackPopupMenuEx(popupMenu, 0, GET_X_LPARAM(lParam),
				GET_Y_LPARAM(lParam), inHWnd, nullptr);
		}
		::DestroyMenu(popupMenu);
	}
}

bool AnalyticBMPApproximation::ProcessCommand(int commandId)
{
	HMENU menu = ::GetMenu(mFrameWindow);

	switch (commandId)
	{
	case IDM_OPEN:
	{
		TCHAR path[MAX_PATH];
		if (LocateFile(path, MAX_PATH) && OpenFile(path))
		{
			mIndexOfScaleMenu = 0;
			::CheckMenuRadioItem(menu, IDM_SCALE1, IDM_SCALEEND, IDM_SCALE1, MF_BYCOMMAND);
			mScale = 1.0;
			mOriginalPixelSize = mImageKeeper.GetSize();
			mScaledSize = mOriginalPixelSize;
			SetScrollBars(0);

			::InvalidateRect(mClientWindow, nullptr, false);
			if (mStatusBarWindow)
			{
				std::string text("image size = ");
				text += std::to_string(mOriginalPixelSize.cx) + " x " +
					std::to_string(mOriginalPixelSize.cy);
				::SetWindowTextA(mStatusBarWindow, text.c_str());
			}
		}
	}
		break;
	case IDM_SCALE1:
	case IDM_SCALE2:
	case IDM_SCALE5:
	case IDM_SCALE10:
	case IDM_SCALE20:
	case IDM_SCALE50:
	{
		::CheckMenuRadioItem(menu, IDM_SCALE1, IDM_SCALEEND, commandId, MF_BYCOMMAND);

		mIndexOfScaleMenu = commandId - IDM_SCALE1;
		double scale = kScales[mIndexOfScaleMenu];
		mScaledSize.cx = int(scale * mOriginalPixelSize.cx);
		mScaledSize.cy = int(scale * mOriginalPixelSize.cy);
		SetScrollBars(scale / mScale);
		SetScaleAndRefresh(scale);
	}

		break;
	case IDM_INTERPOLATE:
	{
		UINT state = ::GetMenuState(menu, IDM_INTERPOLATE, MF_BYCOMMAND);
		state = (~state) & MF_CHECKED;
		mInterpolateOriginalPixels = (MF_CHECKED == state);
		::CheckMenuItem(menu, IDM_INTERPOLATE, (state | MF_BYCOMMAND));

		::InvalidateRect(mClientWindow, NULL, false);
	}
		break;
	case IDM_SHOWSTATUSBAR:
		if (mStatusBarWindow)
		{
			UINT oldState = ::GetMenuState(menu, IDM_SHOWSTATUSBAR, MF_BYCOMMAND);
			UINT newState = (~oldState) & MF_CHECKED;
			::CheckMenuItem(menu, IDM_SHOWSTATUSBAR, (newState | MF_BYCOMMAND));
			::ShowWindow(mStatusBarWindow, newState ? SW_SHOW : SW_HIDE);

			RECT clientRect = { 0 };
			::GetClientRect(mFrameWindow, &clientRect);
			UpdateClientSize(clientRect.right, clientRect.bottom);
		}
		break;
	case IDM_MAGNIFIER:
		{
			UINT state = ::GetMenuState(menu, IDM_MAGNIFIER, MF_BYCOMMAND);
			UINT newState = (~state) & MF_CHECKED;
			ShowMagnifier(MF_CHECKED == newState);
			::CheckMenuItem(menu, IDM_MAGNIFIER, newState | MF_BYCOMMAND);
		}
		break;
	case IDM_ANALYTIC:
	{
		UINT state = ::GetMenuState(menu, IDM_ANALYTIC, MF_BYCOMMAND);
		UINT newState = (~state) & MF_CHECKED;
		mImageKeeper.ShowAnalytic(MF_CHECKED == newState);
		::CheckMenuItem(menu, IDM_ANALYTIC, newState | MF_BYCOMMAND);
	}
		break;
	case IDM_SIDEPIXELS_6:
	case IDM_SIDEPIXELS_8:
	case IDM_SIDEPIXELS_10:
	case IDM_SIDEPIXELS_12:
	{
		static const int kSideInPixels[] = { 6, 8, 10, 12 };
		int pixels = kSideInPixels[commandId - IDM_SIDEPIXELS_6];
		mImageKeeper.SetPixelCountPerMagnifierSide(pixels);
		::CheckMenuRadioItem(menu, IDM_SIDEPIXELS_6, IDM_SIDEPIXELS_12, commandId, MF_BYCOMMAND);
		::InvalidateRect(mImageKeeper.GetMagnifierWindow(), NULL, false);
	}
		break;
	case IDM_DEGREE_6:
	case IDM_DEGREE_9:
	case IDM_DEGREE_12:
	case IDM_DEGREE_15:
	case IDM_DEGREE_18:
	case IDM_DEGREE_21:
	{
		static const int kDegree[] = { 6, 9, 12, 15, 18, 21 };
		mImageKeeper.SetDegree(kDegree[commandId - IDM_DEGREE_6]);
		::CheckMenuRadioItem(menu, IDM_DEGREE_6, IDM_DEGREE_21, commandId, MF_BYCOMMAND);
	}
		break;

	case IDM_PERFORMANCELOG:
		::MessageBoxA(mFrameWindow, mImageKeeper.GetPerformanceLog().c_str(),
			"Performance log", MB_OK);
		break;
	case IDM_CIRCLEGREEN:
	case IDM_CIRCLESMOOTH:
	case IDM_CIRCLEDISCRETE:
	case IDM_CIRCLEDISCRETEFEW:
	{
		CircleAppearanceEnum kTokens[] = { kCircleAppearanceGreenLine, kCircleAppearanceSmoothColors,
			kCircleAppearanceDiscreteColors, kCircleAppearanceFewDiscreteColors
		};
		mImageKeeper.SetCircleAppearance(kTokens[commandId - IDM_CIRCLEGREEN]);
		::CheckMenuRadioItem(menu, IDM_CIRCLEGREEN, IDM_CIRCLEDISCRETEFEW, commandId, MF_BYCOMMAND);
	}
		break;
	case IDM_ABOUT:
		DialogBox(mInstance, MAKEINTRESOURCE(IDD_ABOUTBOX), mFrameWindow, About);
		break;
	case IDM_EXIT:
		DestroyWindow(mFrameWindow);
		break;
	default:
		return false;
	}

	return true;
}

LRESULT AnalyticBMPApproximation::WindowProcedure(HWND hWnd, UINT inMessage, WPARAM wParam, LPARAM lParam)
{
	LRESULT result = 0;

	switch (inMessage)
	{
	case WM_COMMAND:
		if (!ProcessCommand(LOWORD(wParam)))
		{
			result = DefWindowProc(mFrameWindow, inMessage, wParam, lParam);
		}
		break;
	case WM_CONTEXTMENU:
		if ((HWND)wParam == mClientWindow)
		{
			RunContextMenu(1, mClientWindow, lParam);
		}
		else
		{
			result = DefWindowProc(hWnd, inMessage, wParam, lParam);
		}
		break;
	case WM_SIZE:
		if (mStatusBarWindow)
		{
			// move the status bar to its place at the bottom of client area
			result = ::SendMessage(mStatusBarWindow, inMessage, wParam, lParam);
		}
		result = (UpdateClientSize(LOWORD(lParam), HIWORD(lParam)) && 0 == result) ? 0 : 1;
		break;
	case WM_POINTERWHEEL:
		// forward to the client for processing (together with other mouse events)
		result = ::SendMessage(mClientWindow, inMessage, wParam, lParam);
		break;
	case WM_DESTROY:
		if (HWND hMagnifierWnd = mImageKeeper.GetMagnifierWindow())
		{
			::DestroyWindow(hMagnifierWnd);
			mImageKeeper.SetMagnifierWindow(nullptr);
		}
		
		PostQuitMessage(0);
		break;
	default:
		result = DefWindowProc(hWnd, inMessage, wParam, lParam);
	}

	return result;
}

LRESULT AnalyticBMPApproximation::WindowProcedureClient(HWND hWnd, UINT inMessage,
	WPARAM wParam, LPARAM lParam)
{
	LRESULT result = 0;
	switch (inMessage)
	{
	case WM_PAINT:
		if (mImageKeeper.RecreateOriginalRenderTarget(mClientWindow))
		{
			RECT dirtyRect;
			::GetUpdateRect(hWnd, &dirtyRect, false);
			mImageKeeper.DrawOriginal(mScrollPosition,	mScale,
				mInterpolateOriginalPixels, !mIsMouseReflectedInMagnifier,
				dirtyRect);
			::ValidateRect(mClientWindow, NULL);
		}
		break;
	case WM_SIZE:
		mImageKeeper.ResizeOriginalRenderTarget(LOWORD(lParam), HIWORD(lParam));
		result = DefWindowProc(hWnd, inMessage, wParam, lParam);
		// redraw scroll bars...
		break;
	case WM_HSCROLL:
		{
			mScrollPosition.x = UpdateScrollbarPosition(SB_HORZ,
				LOWORD(wParam), HIWORD(wParam));
			::InvalidateRect(hWnd, NULL, false);
		}
		break;
	case WM_VSCROLL:
	{
		mScrollPosition.y = UpdateScrollbarPosition(SB_VERT,
			LOWORD(wParam), HIWORD(wParam));
		::InvalidateRect(hWnd, NULL, false);
	}
	break;
	case WM_COMMAND:
		{
			result = ProcessCommand(LOWORD(wParam)) ? 0 : 1;
		}
		break;
	case WM_POINTERDOWN:
		if (IS_POINTER_FIRSTBUTTON_WPARAM(wParam))
		{
			mLParamAtLMouseDown = lParam;
			mTimeAtLMouseDown = ::GetTickCount();
			mPreviousScreenX = GET_X_LPARAM(lParam);
			mPreviousScreenY = GET_Y_LPARAM(lParam);
		}
		break;
	case WM_POINTERUP:
		{
			// invoke the context menu when needed
			::DefWindowProc(hWnd, inMessage, wParam, lParam);
		}
		if (IS_POINTER_INRANGE_WPARAM(wParam))
		{
			DWORD dt = ::GetTickCount() - mTimeAtLMouseDown;
			const int kClickDurationTolerance = 300; // milliseconds
			if (dt < kClickDurationTolerance)
			{
				int dx = GET_X_LPARAM(lParam) - GET_X_LPARAM(mLParamAtLMouseDown);
				int dy = GET_Y_LPARAM(lParam) - GET_Y_LPARAM(mLParamAtLMouseDown);
				const int kClickDisplacementTolerance = 5; // radius in pixels
				if (dx * dx + dy * dy <= kClickDisplacementTolerance * kClickDisplacementTolerance)
				{
					// mouse click
					mIsMouseReflectedInMagnifier = !mIsMouseReflectedInMagnifier;

					RECT dirtyRect = mImageKeeper.GetLastReflectingPointArea(mScrollPosition, mScale);
					::InvalidateRect(hWnd, &dirtyRect, false);
					if (mIsMouseReflectedInMagnifier)
					{
						ReflectMousePointer(lParam);
					}
				}
			}			
		}
		if (HCURSOR hCursor = ::LoadCursor(NULL, IDC_ARROW))
		{
			::SetCursor(hCursor);
		}
		break;
	case WM_POINTERUPDATE:
		if (IS_POINTER_FIRSTBUTTON_WPARAM(wParam))
		{
			// drag image;
			bool isChange = false;
			
			SCROLLINFO si;
			si.cbSize = sizeof(si);
			si.fMask = SIF_ALL;
			int x = GET_X_LPARAM(lParam);
			if (x != mPreviousScreenX && ::GetScrollInfo(hWnd, SB_HORZ, &si))
			{
				// if the mouse pointer moves right, the origin of image
				// (that at the moment was beyond the left edge of window) also moves right
				// towards decreasing the scroll offset.
				int pos = si.nPos - (x - mPreviousScreenX);
				mPreviousScreenX = x;

				RestrictToBounds(pos, si.nMin, int(si.nMax + 1 - max(si.nPage, 1)));
				if (pos != si.nPos)
				{
					::SetScrollPos(hWnd, SB_HORZ, pos, true);
					mScrollPosition.x = pos;
					isChange = true;
				}
			}
			
			int y = GET_Y_LPARAM(lParam);
			if (y != mPreviousScreenY && ::GetScrollInfo(hWnd, SB_VERT, &si))
			{
				int pos = si.nPos - (y - mPreviousScreenY);
				mPreviousScreenY = y;

				RestrictToBounds(pos, si.nMin, int(si.nMax + 1 - max(si.nPage, 1)));
				if (pos != si.nPos)
				{
					::SetScrollPos(hWnd, SB_VERT, pos, true);
					mScrollPosition.y = pos;
					isChange = true;
				}
			}
			if (isChange)
			{
				if (HCURSOR hCursor = ::LoadCursor(NULL, IDC_SIZEALL))
				{
					::SetCursor(hCursor);
				}

				::InvalidateRect(hWnd, nullptr, false);
			}
		}
		else if (mIsMouseReflectedInMagnifier)
		{
			ReflectMousePointer(lParam);
		}
		break;
	case WM_POINTERWHEEL:
	{
		int x = GET_X_LPARAM(lParam);
		int y = GET_Y_LPARAM(lParam);

		HWND magnifierWindow = mImageKeeper.GetMagnifierWindow();
		if (::IsWindowVisible(magnifierWindow))
		{
			RECT magnifierRect;
			if (::GetWindowRect(magnifierWindow, &magnifierRect) &&
				::PtInRect(&magnifierRect, { x, y }))
			{
				// relay the processing to magnifier window
				::SendMessage(magnifierWindow, inMessage, wParam, lParam);
				break;
			}
		}
		int delta = GET_WHEEL_DELTA_WPARAM(wParam);
		double scale = mScale * (1 + delta * 0.001);
		RestrictToBounds(scale, 1.0, 50.0);
		if (::fabs(scale - mScale) < 0.01)
		{
			break;
		}
		int indexOfScaleMenu = -1;
		for (int i = sizeof(kScales) / sizeof(kScales[0]); i--;)
		{
			if (::fabs(scale - kScales[i]) < 0.03 * kScales[i])
			{
				scale = kScales[i];
				indexOfScaleMenu = i;
				break;
				
				break;
			}
		}
		if (indexOfScaleMenu != mIndexOfScaleMenu)
		{
			mIndexOfScaleMenu = indexOfScaleMenu;
			if (HMENU menu = ::GetMenu(mFrameWindow))
			{
				::CheckMenuRadioItem(menu, IDM_SCALE1, IDM_SCALE50, 
					IDM_SCALE1 + indexOfScaleMenu, MF_BYCOMMAND);
			}
		}


		WINDOWINFO wi;
		wi.cbSize = sizeof(wi);
		if (::GetWindowInfo(hWnd, &wi))
		{
			x -= wi.rcClient.left;
			y -= wi.rcClient.top;
		}

		mScaledSize.cx = int(scale * mOriginalPixelSize.cx);
		mScaledSize.cy = int(scale * mOriginalPixelSize.cy);
		SetScrollBars(scale / mScale, x, y);
		SetScaleAndRefresh(scale);
	}
		break;
	default:
		result = DefWindowProc(hWnd, inMessage, wParam, lParam);
		break;
	}
	return result;
}

LRESULT AnalyticBMPApproximation::WindowProcedureMagnifier(HWND hWnd, UINT inMessage,
	WPARAM wParam, LPARAM lParam)
{
	LRESULT result = 0;
	switch (inMessage)
	{
	case WM_CLOSE:
		// hide window and update menu
		::ShowWindow(hWnd, SW_HIDE);
		if (HMENU menu = ::GetMenu(mFrameWindow))
		{
			::CheckMenuItem(menu, IDM_MAGNIFIER, MF_UNCHECKED | MF_BYCOMMAND);
		}
		break;
	case WM_PAINT:
	{
		RECT dirtyRect;
		::GetUpdateRect(hWnd, &dirtyRect, false);
		result = mImageKeeper.DrawMagnifier(dirtyRect);
		::ValidateRect(hWnd, NULL);
	}
		break;
	case kWmStartUpdatingAnalytics:
		mImageKeeper.StartUpdatingAnalytics(wParam);
		break;
	case kWmStartUpdatingCircle:
		mImageKeeper.StartUpdatingCircle();
		break;
	case WM_COMMAND:
		if (!ProcessCommand(LOWORD(wParam)))
		{
			result = DefWindowProc(hWnd, inMessage, wParam, lParam);
		}
		break;
	case WM_CONTEXTMENU:
		{
			WINDOWINFO wi;
			wi.cbSize = sizeof(wi);
			::GetWindowInfo(hWnd, &wi);
			int x = GET_X_LPARAM(lParam) - wi.rcClient.left;
			int y = GET_Y_LPARAM(lParam) - wi.rcClient.top;

			int position = (mImageKeeper.IsPointInMagnifierSquare(x, y) ?
				2 : 3);
			RunContextMenu(position, hWnd, lParam);
		}
		break;
	case WM_POINTERUPDATE:
	{
		WINDOWINFO wi;
		wi.cbSize = sizeof(wi);
		::GetWindowInfo(hWnd, &wi);
		int x = GET_X_LPARAM(lParam) - wi.rcClient.left;
		int y = GET_Y_LPARAM(lParam) - wi.rcClient.top;
		LPCTSTR cursorId = mImageKeeper.GetMagnifierCursorShape(x, y);
		
		if (HCURSOR cursor = ::LoadCursor(nullptr, cursorId))
		{
			::SetCursor(cursor);
		}
		if (IS_POINTER_INCONTACT_WPARAM(wParam))
		{
			if (mImageKeeper.IsCenterMoving())
			{
				// previous position of the small circle
				RECT r = mImageKeeper.GetLastReflectingPointArea(mScrollPosition, mScale);

				mImageKeeper.ChangeCircle(x, y);
				::InvalidateRect(mClientWindow, &r, false);

				// current position of the small circle
				r = mImageKeeper.GetLastReflectingPointArea(mScrollPosition, mScale);
				::InvalidateRect(mClientWindow, &r, false);

				ShowCoordinatesInStatusBar();
			}
			else
			{
				mImageKeeper.ChangeCircle(x, y);
			}
		}
	}
		break;
	case WM_POINTERDOWN:
	{
		if (!IS_POINTER_INCONTACT_WPARAM(wParam))
		{
			break;
		}
		
		WINDOWINFO wi;
		wi.cbSize = sizeof(wi);
		::GetWindowInfo(hWnd, &wi);
		int x = GET_X_LPARAM(lParam) - wi.rcClient.left;
		int y = GET_Y_LPARAM(lParam) - wi.rcClient.top;
		LPCTSTR cursorId = mImageKeeper.GetMagnifierCursorShape(x, y);

		if (HCURSOR cursor = ::LoadCursor(nullptr, cursorId))
		{
			::SetCursor(cursor);
		}
		if (IS_POINTER_FIRSTBUTTON_WPARAM(wParam))
		{
			// move / resize
			mImageKeeper.SetInitialPoint(x, y);
		}
		else if (IS_POINTER_SECONDBUTTON_WPARAM(wParam))
		{
			/*int position = (mImageKeeper.IsPointInMagnifierSquare(x, y) ?
				2 : 3);
			RunContextMenu(position, lParam);*/
		}
	}
		break;
	case WM_POINTERWHEEL:
	{
		int wheelDelta = GET_WHEEL_DELTA_WPARAM(wParam);
		DWORD time = ::GetTickCount();
		if (time - mTimeOfLastWheelEvent > kMaxRapidWheelInterval)
		{
			mNumberOfRapidWheelEvents = 0;
		}
		mTimeOfLastWheelEvent = time;
		++mNumberOfRapidWheelEvents;
		const double coef = (2 < mNumberOfRapidWheelEvents ? 0.06 : (1.0 / 120));
		mImageKeeper.ChangeCircleRadiusBy(coef * wheelDelta);
	}
		break;
	case WM_KEYDOWN:
	{
		int dx = 0, dy = 0;
		switch (wParam)
		{
			case VK_LEFT: dx = -1; break;
			case VK_RIGHT: dx = 1; break;
			case VK_UP: dy = -1; break;
			case VK_DOWN: dy = 1; break;
		}
		if (0 != dx || 0 != dy)
		{
			RECT r = mImageKeeper.GetLastReflectingPointArea(mScrollPosition, mScale);
			mImageKeeper.MoveCircleBy(dx, dy);
			::InvalidateRect(mClientWindow, &r, false);
			r = mImageKeeper.GetLastReflectingPointArea(mScrollPosition, mScale);
			::InvalidateRect(mClientWindow, &r, false);

			ShowCoordinatesInStatusBar();
		}
		else
		{
			result = ::DefWindowProc(hWnd, inMessage, wParam, lParam);
		}
	}
		break;
	default:
		return DefWindowProc(hWnd, inMessage, wParam, lParam);
	}

	return result;
}

// static
LRESULT AnalyticBMPApproximation::WndProcCommon(HWND inHWnd,
	UINT inMessage, WPARAM wParam, LPARAM lParam, LocalProcedure inProcedure)
{
	LRESULT result = 0;
	if (auto pThis = reinterpret_cast<AnalyticBMPApproximation *>(
		GetWindowLongPtr(inHWnd, GWLP_USERDATA)))
	{
		return (pThis->*inProcedure)(inHWnd, inMessage, wParam, lParam);
	}

	if (WM_CREATE == inMessage)
	{
		auto psc = reinterpret_cast<LPCREATESTRUCT>(lParam);
		LONG_PTR pThisAsLongPtr = reinterpret_cast<LONG_PTR>(psc->lpCreateParams);
		SetWindowLongPtr(inHWnd, GWLP_USERDATA, pThisAsLongPtr);
	}
	return DefWindowProc(inHWnd, inMessage, wParam, lParam);
}

LRESULT CALLBACK AnalyticBMPApproximation::WndProcFrame(HWND inHWnd,
	UINT inMessage, WPARAM wParam, LPARAM lParam)
{
	return WndProcCommon(inHWnd, inMessage, wParam, lParam, 
		&AnalyticBMPApproximation::WindowProcedure);
}

LRESULT CALLBACK AnalyticBMPApproximation::WndProcClient(HWND inHWnd,
	UINT inMessage, WPARAM wParam, LPARAM lParam)
{
	return WndProcCommon(inHWnd, inMessage, wParam, lParam, 
		&AnalyticBMPApproximation::WindowProcedureClient);
}
LRESULT CALLBACK AnalyticBMPApproximation::WndProcMagnifier(HWND inHWnd,
	UINT inMessage, WPARAM wParam, LPARAM lParam)
{
	return WndProcCommon(inHWnd, inMessage, wParam, lParam, 
		&AnalyticBMPApproximation::WindowProcedureMagnifier);
}

// Message handler for about box.
INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
	UNREFERENCED_PARAMETER(lParam);
	switch (message)
	{
	case WM_INITDIALOG:
		return (INT_PTR)TRUE;

	case WM_COMMAND:
		if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
		{
			EndDialog(hDlg, LOWORD(wParam));
			return (INT_PTR)TRUE;
		}
		break;
	}
	return (INT_PTR)FALSE;
}
